<template>
    <div class="micro-app-component-wrapper">
        <component
            :is="microTagName"
            v-if="appUrl && componentUrl"
            :name="microAppName"
            :url="appUrl"
            :baseroute="baseRoute"
            :data="toRaw(microData)"
            :style="{
                height: '100%'
            }"
            @created="microCreated"
            @beforemount="microBeforemount"
            @mounted="microMounted"
            @unmount="microUnmount"
            @error="microError"
            @datachange="microDataChangeHandle"
        />

        <div v-if="loadError" class="micro-error-wrapper">
            <MicroAppErr :app-name="`Route Path：${componentUrl} `" />
        </div>

        <g-loding-shade :show="loadingFlag" />
    </div>
</template>

<script lang="ts">
export default {
    name: 'RemoteMicroAppComponent'
}
</script>

<script lang="ts" setup>
import { computed, ref, watch, toRaw, onUnmounted, onBeforeUnmount } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import MicroAppErr from '@jsjn/micro-core-views/error/microAppErr.vue'
import { useStore } from '@jsjn/micro-core-store'
import {
    interceptorsReqHandle,
    interceptorsResHandle
} from '@jsjn/micro-core-micro-main/httpInterceptors'
import microTagName from '@jsjn/micro-core-micro-main/micro-app/currentMicroTagName'
import { routeMode } from '@jsjn/micro-core-micro-main/routeMode'
import SendMicroAppData from '../interface/SendMicroAppData'
import AcceptMicroAppData from '../interface/AcceptMicroAppData'
import FIRST_NEST_MICRO_KEY from '../constant/FIRST_NEST_MICRO_KEY'
import { Session } from '@jsjn/utils'
import { v4 as uuidv4 } from 'uuid'
import packInfo from '@ROOT/package.json'
import _ from 'lodash'

/**
 * 用来存储每一次打开的嵌套应用的基础路由的 key
 * 相当于每一次打开的嵌套微应用都是唯一的
 */
const CURRENT_ROUTE_CACHE_KEY = `${packInfo.name.toUpperCase()}_CURRENT_ROUTE_CACHE_KEY_${uuidv4()
    .replace(/\-/g, '')
    .toUpperCase()}`

const props = withDefaults(
    defineProps<{
        /**
         * 微应用的地址，仅做资源加载的地址
         */
        appUrl: string
        /**
         * 微应用内注册的组件的路由地址
         */
        componentUrl: string
        /**
         * 传递给目标路由组件的数据（path + querty）
         */
        data?: object
    }>(),
    {
        appUrl: '',
        componentUrl: '',
        data: null
    }
)

const emits = defineEmits(['created', 'beforemount', 'mounted', 'unmount', 'error', 'datachange'])

const store = useStore()
const route = useRoute()
const router = useRouter()

/**
 * 所有的微应用都是根据路由 url 去匹配的
 * 嵌套应用之间，多个共存的应用是共享一条 url 的
 * 所以，应用之间要体现出加载顺序
 *
 * 加载一个嵌套应用时，需要将当前的路由状态改变
 * 改变后的路由，将作为下一个嵌套应用的基础路由挂载
 *
 * 这样前一个应用改变路由（注册时要模糊匹配），为下一个嵌套应用铺垫路径
 */
const baseRoute = _.clone(route.path)

/**
 * 存储当前加载的嵌套微应用的基础路由（即当前页面的路由）
 * 用来恢复真实的路由
 *
 * 我们将微应用组件路由的跳转，视为无意义的，假的（没有映射到基座应用中）
 *
 * 在存储当前路由信息时，路由有可能携带参数，目前仅支持 query 的存储
 * params 不再处理
 *
 * 一次性存储，避免多个微应用的组件互相覆盖
 */
if (Session.get(CURRENT_ROUTE_CACHE_KEY) === null) {
    const { path, query } = route
    const currentRouteCache = {
        path,
        query
    }

    Session.set(CURRENT_ROUTE_CACHE_KEY, currentRouteCache)
}

/**
 * 存储所有微应用（嵌套的）的首次加载
 * 首次加载是以某一个页面为活动周期的：
 *  a 页面嵌套了 n 个子应用
 *  一旦切换到 b 页面，将视为从新开始
 */
if (Session.get(FIRST_NEST_MICRO_KEY) === null) {
    const { path, query } = route
    const currentRouteCache = {
        path,
        query
    }

    Session.set(FIRST_NEST_MICRO_KEY, currentRouteCache)
}

/**
 * 应用的 name 变得不再重要，保证唯一即可
 */
const microAppName = `random-id-${uuidv4().replace(/-/g, '')}`
// 将当前路由 + 远程组件的 url 拼接成最终的定位路由
const targetComponentPath = computed(() => `${route.path}${props.componentUrl}`)

// 页面加载出错
const loadError = ref<boolean>(false)
// 微应用加载中
const loadingFlag = ref<boolean>(false)

/**
 * 发送给微应用的数据
 *
 * - vuex
 * - 请求拦截器处理函数
 * - 路由模式
 */
const microData = ref<SendMicroAppData>({
    vuex: null,
    interceptorsReqHandle,
    interceptorsResHandle,
    routeMode
})
watch(
    () => store.state,
    (state) => {
        const copyState = _.cloneDeep(toRaw(state))

        /**
         * 作为嵌套微应用，应该保证基座应用的 registeredRoutes 被正确传递
         * 因为在微应用接收的时候，将基座应用的 注册路由作为 remoteRegisteredRoutes 存储
         * 但是传递给嵌套子应用的实际为 registeredRoutes
         * 这边进行二者的同化即可
         */
        copyState.appFuncTree.registeredRoutes = copyState.appFuncTree.remoteRegisteredRoutes

        microData.value = {
            ...microData.value,
            vuex: copyState
        }
    },
    {
        deep: true
    }
)

// 接收子应用的数据
const microDataChangeHandle = (e) => {
    emits('datachange', e.detail.data)
}

// 微应用创建
const microCreated = () => {
    loadingFlag.value = true
    loadError.value = false
    emits('created')
}

// 微应用挂载前
const microBeforemount = () => {
    /**
     * 挂载：
     *  在微应用挂载前，将父级（当前微应用）的路由定向到目标组件的路由
     *  在微应用初始挂载后，就会定位到对应的组件路由
     *  远程组件和页面不同的是，远程组件的加载是明确的，且只有一次的，因此，我们可以通过首次加载，去满足组件的加载
     *  远程组件是基本不需要进行路由切换的
     *
     * 参数传递：
     *  和远程组件的交互，数据经由 url 传递
     *  参数的获取&传递，应当遵循组件的设计思想，避免和框架产生过多交互，采用路由传参有：
     *      - 以 query 传参，参数可以保留在标签页中
     *      - 可以防止刷新
     *      - 组件获取方便（通过 router 获取）
     *      - 和框架减少交互（如果存储在 microApp 中，将频繁和框架交互）
     *
     * 独立路由：
     *  在这里，没有改变基座应用的路由，行为上来说，我们只是想加载一个组件
     *  不应该改变基座应用的路由（基座应用的路由，会匹配 tagsView 标签页、菜单等）
     *  本身基座应用和微应用的 router 实例是多个的，一个思想：
     *      - 嵌套微应用只改变自己的路由状态，不会抛出
     *  就像平常开发，引用组件一样，避免对基座应用的路由对象产生影响
     */
    router.replace({
        path: targetComponentPath.value,
        query: props.data as any
    })

    emits('beforemount')
}

watch(
    () => props.data,
    (data) => {
        microData.value = {
            ...microData.value,
            ...data
        }
    },
    { deep: true, immediate: true }
)

// 微应用挂载后
const microMounted = () => {
    loadingFlag.value = false
    emits('mounted')
}

// 微应用卸载
const microUnmount = () => {
    const currentRouteCache = Session.get(CURRENT_ROUTE_CACHE_KEY)
    /**
     * 当嵌套微应用卸载时，应当恢复加载嵌套应用前的路由
     * 因为嵌套应用加载的根本：是将当前微应用的路由变化（添加），适应子微应用的路由
     * 所以，加载嵌套微应用的地方的路由一定是包含当前微应用的路由的
     *
     * 所以，用加载嵌套应用的路由去匹配初始（加载嵌套应用前）缓存路由，是 !一定可以匹配到的!
     * 如果匹配出错，可以从这里查找问题
     */
    if (currentRouteCache && route.path.startsWith(currentRouteCache.path)) {
        router.replace(currentRouteCache)
        Session.remove(CURRENT_ROUTE_CACHE_KEY)
    }

    emits('unmount')
}

// 微应用加载失败
const microError = () => {
    loadingFlag.value = false
    loadError.value = true
    emits('error')
}

// 容错处理：基座应用尝试加载微应用资源，加载出错，则显示错误页
watch(
    () => props.appUrl,
    (url) => {
        if (!props.appUrl) return
        fetch(`${props.appUrl}/index.html`)
            .then((res) => {})
            .catch((err) => {
                loadingFlag.value = false
                loadError.value = true
                emits('error')
            })
    },
    {
        immediate: true
    }
)
</script>

<style lang="scss" scoped>
/*! scopecss-disable */
.micro-app-component-wrapper {
    position: relative;
    width: 100%;
    height: 100%;

    .micro-error-wrapper {
        position: absolute;
        width: 100%;
        height: 100%;
        top: 0;
        left: 0;
        padding: 2px;
        background: #fff;
        display: flex;
        align-items: center;
        justify-content: center;

        :deep(h1) {
            margin: 0;
        }
    }
}
</style>
